﻿//全景图切图
using SkiaSharp;
using System.Numerics;

Console.WriteLine("""
    =====================================================================================================
    *                   全景图切六面图工具                        --from jackletter 20230320            *
    *    示例: PanoramaCut.exe model=1 width=1500 panorama.jpg                                          *
    *    参数:                                                                                          *
    *        model: 1/2/3, 默认是1                                                                      *
    *               1: 全景图中心为前                                                                   *
    *               2: 全景图中心为后                                                                   *
    *               3: 全景图中心为后,且转换完成后将左右面文件名字对调                                  *
    *        width: 默认1500                                                                            *
    *        panorama.jpg: 可以是多个文件夹或文件的名称, 必须是 ".jpg" 结尾的文件且不能类似 "*_l.jpg"   *
    *    输出: 在 panorama.jpg 同目录会输出 "panorama_f.jpg"、"panorama_b.jpg" 等六个文件, 同名直接覆盖.*
    *    其他: model和width可以通过 PanoramaCut.config 启动时加载,  PanoramaCut.config示例:             *
    *               width=1500                                                                          *
    *               model=3                                                                             *
    =====================================================================================================

    """);

var srcPaths = new List<string>();

var model = 1;
var imgWidth = 1500;

var dir = AppDomain.CurrentDomain.BaseDirectory;
var configPath = Path.Combine(dir, "PanoramaCut.config");
if (File.Exists(configPath))
{
    var lines = File.ReadAllLines(configPath).Select(i => i.Trim()).Where(i => i.StartsWith("model", StringComparison.OrdinalIgnoreCase) || i.StartsWith("width", StringComparison.OrdinalIgnoreCase)).ToList();
    var modelLine = lines.FirstOrDefault(i => string.Equals(i.Split('=').FirstOrDefault(), "model", StringComparison.OrdinalIgnoreCase));
    if (modelLine != null)
    {
        var modelStr = modelLine.Split('=').LastOrDefault();
        _ = int.TryParse(modelStr, out model);
    }
    var widthLine = lines.FirstOrDefault(i => string.Equals(i.Split('=').FirstOrDefault(), "width", StringComparison.OrdinalIgnoreCase));
    if (widthLine != null)
    {
        var widthStr = widthLine.Split('=').LastOrDefault();
        _ = int.TryParse(widthStr, out imgWidth);
    }
}

if (args.Length == 0)
{
    Console.WriteLine("未指定文件或路径!");
    return;
}
for (int i = 0; i < args.Length; i++)
{
    if (Directory.Exists(args[i]))
    {
        var files = Directory.GetFiles(args[i])
            .Where(i => i.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase))
            .Where(i => !"_f,_b,_l,_r,_u,_d".Split(',').Any(ext => Path.GetFileNameWithoutExtension(i).EndsWith(ext)))
            .ToArray();
        srcPaths.AddRange(files);
    }
    else if (File.Exists(args[i]) && args[i].EndsWith(".jpg", StringComparison.OrdinalIgnoreCase))
    {
        srcPaths.Add(args[i]);
    }
    else if (args[i].StartsWith("model="))
    {
        var modelStr = args[i].Replace("model=", "");
        model = modelStr == "1" ? 1 : modelStr == "2" ? 2 : modelStr == "3" ? 3 : 1;
    }
    else if (args[i].StartsWith("width="))
    {
        var widthStr = args[i].Replace("width=", "");
        if (int.TryParse(widthStr, out var width))
        {
            imgWidth = width;
        }
        else
        {
            Console.WriteLine($"参数错误,示例: Panorama.exe width=1500 panarama.jpg");
            return;
        }
    }
}
Console.WriteLine($"待切图图片有 {srcPaths.Count} 个, model={model} width={imgWidth}");
Console.WriteLine();
for (int i = 0; i < srcPaths.Count; i++)
{
    var imgPath = srcPaths[i];
    Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {i + 1}/{srcPaths.Count} {imgPath}");
    var outDir = Path.GetDirectoryName(imgPath);
    var panoramaName = Path.GetFileNameWithoutExtension(imgPath);
    using var img = SKBitmap.Decode(imgPath);
    var faces = getFaces(model);
    for (int f = 0; f < faces.Length; f++)
    {
        var (tag, origin, uend, vend) = faces[f];
        var faceName = $"{panoramaName}_{tag}.jpg";
        Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}     {f + 1}/{faces.Length} face={faceName}");
        var u = (uend - origin) / imgWidth;
        var v = (vend - origin) / imgWidth;
        using var dest = new SKBitmap(imgWidth, imgWidth);
        for (int ii = 0; ii < imgWidth; ii++)
        {
            for (int jj = 0; jj < imgWidth; jj++)
            {
                var point = origin + u * ii + v * jj;
                var (longitude, latitude) = getLngLat(point);
                var uv = getUV(longitude, latitude);
                var color = BilinearInterpolation(img, uv.u * img.Width, uv.v * img.Height);
                dest.SetPixel(ii, imgWidth - 1 - jj, color);
            }
        }
        var destPath = Path.Combine(outDir!, faceName);
        if (File.Exists(destPath)) File.Delete(destPath);
        using var fs = new FileStream(destPath, FileMode.CreateNew);
        dest.Encode(fs, SKEncodedImageFormat.Jpeg, 80);
    }
}

Console.WriteLine($"全部完成!");
Console.Read();

(string tag, Vector3 origin, Vector3 uend, Vector3 vend)[] getFaces(int model)
{
    /*
     *            7--------------6
     *           /.             /|
     *          / .            / |
     *         /  .           /  |
     *        /   .          /   |
     *       2--------------3    |
     *       |    5 . . . . |. . 4
     *       |   .          |   /
     *       |  .           |  /
     *       | .            | /
     *       |.             |/
     *       0--------------1
     * 
     */
    var points = new List<Vector3>
    {
        new(-1,-1,1),//0
        new(1,-1,1),//1
        new(-1,1,1),//2
        new(1,1,1),//3
        new(1,-1,-1),//4
        new(-1,-1,-1),//5
        new(1,1,-1),//6
        new(-1,1,-1),//7
    };
    (string tag, Vector3 origin, Vector3 uend, Vector3 vend)[] panoramaImageCenterIsFront =
        [
            ("f", points[5], points[4], points[7]),//前
            ("b", points[1], points[0], points[3]),//后
            ("l", points[0], points[5], points[2]),//左
            ("r", points[4], points[1], points[6]),//右
            ("u", points[7], points[6], points[2]),//上
            ("d", points[0], points[1], points[5]),//下
        ];
    (string tag, Vector3 origin, Vector3 uend, Vector3 vend)[] panoramaImageCenterIsBack =
        [
            ("f", points[1], points[0], points[3]),//前
            ("b", points[5], points[4], points[7]),//后
            ("l", points[4], points[1], points[6]),//左
            ("r", points[0], points[5], points[2]),//右
            ("u", points[3], points[2], points[6]),//上
            ("d", points[4], points[5], points[1]),//下
        ];
    if (model == 1) return panoramaImageCenterIsFront;
    else if (model == 2) return panoramaImageCenterIsBack;
    else if (model == 3)
    {
        panoramaImageCenterIsBack[2].tag = "r";
        panoramaImageCenterIsBack[3].tag = "l";
        return panoramaImageCenterIsBack;
    }
    throw new Exception("bad model");
}

//双线性插值, x值域[0,image.Width], y值域[0,image.Height]
//注意: y轴正方向朝下(因为 SKBitmap.GetPixel(x, y)就是如此)
SKColor BilinearInterpolation(SKBitmap image, double x, double y)
{
    int x1 = (int)Math.Floor(x);
    int y1 = (int)Math.Floor(y);
    if (x1 >= image.Width)
        x1 = image.Width - 1;
    if (y1 >= image.Height)
        y1 = image.Height - 1;

    int x2 = x1 + 1;
    int y2 = y1 + 1;

    if (x2 >= image.Width)
        x2 = image.Width - 1;
    if (y2 >= image.Height)
        y2 = image.Height - 1;

    var q11 = image.GetPixel(x1, y1);
    var q12 = image.GetPixel(x1, y2);
    var q21 = image.GetPixel(x2, y1);
    var q22 = image.GetPixel(x2, y2);

    double fx = x - x1;
    double fy = y - y1;

    double r = (1 - fx) * (1 - fy) * q11.Red + fx * (1 - fy) * q21.Red + (1 - fx) * fy * q12.Red + fx * fy * q22.Red;
    double g = (1 - fx) * (1 - fy) * q11.Green + fx * (1 - fy) * q21.Green + (1 - fx) * fy * q12.Green + fx * fy * q22.Green;
    double b = (1 - fx) * (1 - fy) * q11.Blue + fx * (1 - fy) * q21.Blue + (1 - fx) * fy * q12.Blue + fx * fy * q22.Blue;

    return new SKColor((byte)r, (byte)g, (byte)b);
}

(float u, float v) getUV(float longitude, float latitude)
{
    return (Math.Max(0, Math.Min(1, longitude / 360)), Math.Max(0, Math.Min(1, latitude / 180)));
}

//返回点相对于原点所在的经纬度, 经度取值 [0,360) 维度取值 [0,180]
//注意: 经度是俯视图相对于 +z 轴顺时针为正; 维度是 +y 轴为0, -y 轴为180
(float longitude, float latitude) getLngLat(Vector3 point)
{
    //经度计算(俯视图): oz 为零度经线, 顺时针为正, 角 zop 为经度, 值域 [0,360] 
    /*
     *           |
     *     ------o----------+x
     *          /|
     *         / |
     *        /  |
     *       p   |
     *           |
     *           +z
     * 
     */
    var longitude = (Math.Atan2(-point.X, point.Z) / Math.PI * 180 + 360) % 360;


    //维度计算: 三维中的点 p 在 xz 平面上的投影是点 A, 维度是角 PoA; +y 轴为0, -y 轴为180
    /* 先根据 xz 坐标求出 OA 的长度,然后 PA 是 y,则角 PoA 很简单
     *                 p
     *          +y     /|
     *          |    / |
     *          |   /  |
     *          |  /   |
     *          | /    |
     *          |/     |
     *          o------|---x------->
     *         /       |  /
     *        /        | /
     *       /         |/
     *      z----------A
     *     /
     *    /
     */
    var r = new Vector2(point.X, point.Z).Length();
    var latitude = -Math.Atan2(point.Y, r) / Math.PI * 180;
    latitude += 90;
    return ((float)longitude, (float)latitude);
}